package com.hucsmn.five;

import java.util.Arrays;
import java.util.Scanner;

/**
 * 五子棋棋盘类。
 */
public class Board {
    // 五子棋胜利条件。
    public static final int WIN_THRESHOLD = 5;

    // 棋盘尺寸参数和落子状态。
    private final int width;
    private final int height;
    private final Slot[][] board;

    // 打印棋盘需要用到的量。
    private final int cellWidth;
    private final String[] displayLabels;

    // 对弈过程状态。
    private boolean terminted = false;
    private boolean blackRound = true;
    private int cursorLine = 0;
    private int cursorCol = 0;

    /**
     * 创建新的 16x16 大小的五子棋棋盘，由黑方执先手。
     *
     * @throws IllegalArgumentException 棋盘太小或者为负数时报错。
     */
    public Board() {
        this(16, 16);
    }

    /**
     * 创建新的五子棋棋盘，由黑方执先手。
     *
     * @param width  棋盘宽度，至少为 5。
     * @param height 棋盘高度，至少为 5。
     * @throws IllegalArgumentException 棋盘太小或者为负数时报错。
     */
    public Board(int width, int height) {
        if (width < 5 || height < 5) {
            throw new IllegalArgumentException("棋盘太小，不能下五子棋");
        }

        // 计算单元宽度和坐标轴标签。
        int labelMax = Math.max(width, height);
        int cellWidth = Slot.DISPLAY_WIDTH;
        String[] displayLabels = new String[labelMax];
        for (int i = 0; i < labelMax; i++) {
            displayLabels[i] = Integer.toHexString(i);
            cellWidth = Math.max(cellWidth, displayLabels[i].length());
        }

        // 初始化棋盘。
        Slot[][] board = new Slot[height][width];
        for (Slot[] line : board) {
            Arrays.fill(line, Slot.EMPTY);
        }

        this.board = board;
        this.width = width;
        this.height = height;
        this.cellWidth = cellWidth;
        this.displayLabels = displayLabels;
    }

    /**
     * 重置棋局。
     */
    public void reset() {
        terminted = false;
        blackRound = true;
        cursorLine = 0;
        cursorCol = 0;
        for (Slot[] line : board) {
            Arrays.fill(line, Slot.EMPTY);
        }
    }

    /**
     * 绘制棋盘。
     */
    public void display() {
        // 列坐标。
        cellPrint("");
        for (int i = 0; i < width; i++) {
            cellPrint(displayLabels[i]);
        }
        System.out.println();

        // 行坐标和棋盘状态。
        for (int i = 0; i < height; i++) {
            cellPrint(displayLabels[i]);
            for (Slot cell : board[i]) {
                cellPrint(cell);
            }
            System.out.println();
        }
    }

    private void cellPrint(Slot cell) {
        cellPrint(String.valueOf(cell));
    }

    private void cellPrint(String cell) {
        System.out.print(cell);
        for (int i = cell.length(); i <= cellWidth; i++) {
            System.out.print(" ");
        }
    }

    /**
     * 提示本回合黑方或白方下棋，并重新绘制棋盘。
     *
     * @return 本回合落子方取得胜利、放弃比赛，或比赛已经结束时返回真。
     */
    public boolean nextRound() {
        if (terminted) {
            System.out.println("比赛已经结束，不再进行下回合！");
            return true;
        }

        String player = "白方";
        if (blackRound) {
            player = "黑方";
        }
        System.out.println("当前回合由" + player + "落子。");

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.print("请" + player + "指定落子的行和列：");
            try {
                String op = sc.nextLine().trim();
                switch (op.toLowerCase()) {
                    case "quit":
                    case "exit":
                    case "end":
                    case "terminate":
                    case "q":
                        System.out.println(player + "已放弃比赛。");
                        terminted = true;
                        return true;
                }

                Parser p = new Parser(op);
                putSlot(p.line, p.col);

                System.out.println(player + "已落子，落子后的棋盘如下：");
                display();

                break;
            } catch (IllegalArgumentException ex) {
                System.out.println(ex.getMessage() + "，请重新输入！");
            }
        }

        terminted = checkWin();
        if (terminted) {
            System.out.println("游戏结束，恭喜" + player + "获得胜利！");
        } else {
            System.out.println("继续下一回合。");
        }
        return terminted;
    }

    /**
     * 当前回合落子方在给定位置落子。
     *
     * @param i 落子行数。
     * @param j 落子列数。
     * @throws IllegalArgumentException 位置不合法，或者该位置已经落子。
     */
    private void putSlot(int i, int j) {
        if (i < 0 || i >= height || j < 0 || j >= width) {
            throw new IllegalArgumentException("不能下在棋盘外面");
        }
        if (board[i][j] != Slot.EMPTY) {
            throw new IllegalArgumentException("不能下在已经落子的位置");
        }

        if (blackRound) {
            board[i][j] = Slot.BLACK;
        } else {
            board[i][j] = Slot.WHITE;
        }

        this.cursorLine = i;
        this.cursorCol = j;
        this.blackRound = !blackRound;
    }

    /**
     * 检查当前落子位置是否连成了五子，是的话本回合落子方胜利。
     *
     * @return 本回合落子方是否取得胜利。
     */
    private boolean checkWin() {
        return checkSame(1, 0)
                || checkSame(0, 1)
                || checkSame(1, 1)
                || checkSame(1, -1);
    }

    private boolean checkSame(int lineStep, int colStep) {
        int same = 1;
        Slot slot = board[cursorLine][cursorCol];

        // 正向扫描。
        int i = cursorLine + lineStep;
        int j = cursorCol + colStep;
        while (i >= 0 && i < height && j >= 0 && j < width) {
            if (board[i][j] != slot) {
                break;
            }
            same += 1;
            i += lineStep;
            j += colStep;
        }

        // 反向扫描。
        i = cursorLine - lineStep;
        j = cursorCol - colStep;
        while (i >= 0 && i < height && j >= 0 && j < width) {
            if (board[i][j] != slot) {
                break;
            }
            same += 1;
            i -= lineStep;
            j -= colStep;
        }

        return same >= WIN_THRESHOLD;
    }
}
